/**
* Abstract: 2D Canvas Rendering Device
*
* Version: 1.0
*
* Disclaimer: IMPORTANT:  This Gamenova software is supplied to you by
* 4E Studios Srl("4E") in consideration of your agreement to the following terms,
* and your use, installation, modification or redistribution of this 4E software
* constitutes acceptance of these terms.  If you do not agree with these terms,
* please do not use, install, modify or redistribute this 4E software.
*
* In consideration of your agreement to abide by the following terms, and subject
* to these terms, 4E grants you a personal, non-exclusive license, under
* 4E's copyrights in this original 4E software (the "4E Software"), to
* use, reproduce, modify and redistribute the 4E Software, with or without
* modifications, in source and/or binary forms; provided that if you redistribute
* the 4E Software in its entirety and without modifications, you must retain
* this notice and the following text and disclaimers in all such redistributions
* of the 4E Software.
* Neither the name, trademarks, service marks or logos of 4E Studios Srl may be used
* to endorse or promote products derived from the 4E Software without specific
* prior written permission from 4E.  Except as expressly stated in this notice,
* no other rights or licenses, express or implied, are granted by 4E herein,
* including but not limited to any patent rights that may be infringed by your
* derivative works or by other works in which the 4E Software may be
* incorporated.
*
* The 4E Software is provided by 4E on an "AS IS" basis.  4E MAKES NO
* WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED
* WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE, REGARDING THE 4E SOFTWARE OR ITS USE AND OPERATION ALONE OR IN
* COMBINATION WITH YOUR PRODUCTS.
*
* IN NO EVENT SHALL 4E BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
* GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR
* DISTRIBUTION OF THE 4E SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF
* CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF
* 4E HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* Copyright (C) 2011 4E Studios Srl All Rights Reserved.
*/

/**

    Sprite Animation Class
    @constructor
    @param {Object} sprite  Sprite of this animation.
    @param {number} nf Number of frames for the animation. Default nf=1 .


*/
function SpriteAnimation2D(sprite, nf) {
    /**
        Sprite of this.
    */
    this.sprite = sprite;

    /**
        Number of frames in this. Default 1.
    */
    this.numFrames = (nf || (nf = 1));

    /**
        Array of franes for this.
    */
    this.frames = new Array(this.numFrames);

    /**
        Current animation frame.
    */
    this.curFrame = 0;

    /**
        Animation is playing?
    */
    this.isPlaying = false;

    /**
        Lenght of the animation in msec
    */
    this.length = 500.0;

    /**
        the animation must loop?
    */
    this.loop = false;

    /**
        Length of one animation frame in msec.
    */
    this.frameLength = this.length / this.numFrames;

    /**
        Actual animation frame.
    */
    this.actualTime = 0.0;

    /**
        User defined callback, called at the end of the animation.
        
    */
    this.onAnimationEnd = function(sprite){};

    /**
        Starts the animation.
        
    */
    this.play = function() {

        this.isPlaying = true;

        this.sprite.animated = true;
    };

    /**
        Stops the animation.
        
    */
    this.stop = function()
    {
        this.isPlaying = false;

        this.sprite.animated = false;
    };

    /**
        Reset the animation to the first frame and stop it.
        
    */
    this.reset = function()
    {
        this.stop();
        this.curFrame = 0;
    }

    /**
        Updates the animation given the elapsed time
        
        @param {number} dt  Elapsed time.
    */
    this._updateSpriteAnimation = function(dt)
    {
        this.actualTime += dt;

        if (this.actualTime >= this.frameLength)
        {
            this.curFrame++;

            this.actualTime = 0;

            if (this.curFrame >= this.frames.length)
            {
                if (this.loop == false)
                {

                    this.reset();

                    this.onAnimationEnd(this.sprite);
                }
                else
                {
                    
                    this.curFrame = 0;
                }

            }
        }


    };

    /**
        Actual animation update function called.
    */
    this.update = this._updateSpriteAnimation;

    /*
        Updates the length of the Animation
        
        @param {number} length   New animation time in msecs.
    */
    this.updateLength = function(length)
    {
        this.length = (length || (length = 1));

        this.frameLength = this.length / this.numFrames;
    };

    /**
        Set the frame for this animation
        
        @param {number} index   Index of the frame to set.
        @param {Object} frame   Frame Object to set.
    */
    this.setFrame = function(index, frame)
    {
        index = index < 0 ? 0 : (index > this.frames.length - 1 ? this.frames.length - 1 : index);

        this.frames[index] = frame;
    };


    /**
        Set a series of frames for this animation
        
        @param {Object} frames  Array of frame objects.
    */
    this.setFrames = function(frames)
    {

        if (!frames)
        {
            GN['Debug'].log('SetFrames: frames is null');
        }

        this.numFrames = frames.length || 1;

        this.frames = frames;

        this.frameLength = this.length / this.numFrames;

    };

};


/**
    Sprite Class
    @constructor
    @param {Object} image    Image Object of the sprite
    @param {string} spriteName  Actual image of the sprite
    @param {number} w   Width of the sprite in pixels.
    @param {number} h   Height of the sprite in pixels.
*/
function Sprite(animationTable, spriteName, w, h)
{
    this.name = spriteName;
    //this.image  = image;
    this.width = w;
    this.height = h;
    this.anims = new Array();
    this.curAnim = null;
    this.anims = {};
    this.x = 0;
    this.y = 0;
    this.alpha = 1.0;

    /**
        sprites drawing pivot. Default in the top-left corner.
    */
    this.pivot = { x: 0 , y: 0};

    this.EMPTY_FUNCTION = function(){};

   // this.SpriteAnimation2D = SpriteAnimation2D;

    /**
        Sets the sprite's pivot position
        
        @param {number} x X coordinate of the pivot.
        @param {number} y Y coordinate of the pivot.
    
    this.setPivot = function(x,  y)
    {
        this.pivot.x = x;
        this.pivot.y = y;
    };
    */


    if(animationTable)
    for(var animk in animationTable){
     
        var animImages = animationTable[animk];
      
        if(this.curAnim == null)this.curAnim = animk;

        this.anims[animk] = new SpriteAnimation2D(this,animImages.length);
        this.anims[animk].setFrames(animImages);

    }
  

    /**
        Returns the screen rect of the sprite
        
        @return {Object} The rect object of the sprite.
    */
    this.rect = function()
    {
        return { x: this.x + this.pivot.x, y: this.y + this.pivot.y, width: this.width, height: this.height};
    };

    /**
        Debug drawing function
        
    */
    this.debugDraw = function()
    {



        this.GNGraphics.context.strokeStyle ="rgb(0, 255, 0)";
        this.GNGraphics.context.strokeRect(this.x, this.y , this.width, this.height);
     

    
    };

    /**

        Draws the sprite on screen
        
    */
    this.draw = function()
    {
     

        this.GNGraphics.drawCallsCnt++;
                    
        var anim = this.anims[this.curAnim];
        var frame = anim.frames[anim.curFrame];

      
         this.debugDraw();

        this.GNGraphics.context.globalAlpha  = this.alpha;

        this.GNGraphics.context.drawImage(frame,0,0,frame.width,frame.height,this.x, this.y,this.width,this.height);
                       

       
    };

    // stores the old draw function
    this._oldDraw = this.draw;

    

    // memorizza la vecchia debug draw
    this._oldDebugDraw = this.debugDraw;

    /**
        Shows the debug gizmo
        
    */
    this.showDebug = function()
    {
        this.debugDraw = this._oldDebugDraw;
    };

    /**
        Hides the debug gizmo
        
    */
    this.hideDebug = function()
    {

        if (this.debugDraw != this.EMPTY_FUNCTION)
        {
            this._oldDebugDraw = this.debugDraw;

            this.debugDraw = this.EMPTY_FUNCTION;
        }

    };

    // default, disables the debug gizmo
    this.hideDebug();


    /**
        Hides the sprite
        
    */
    this.hide = function()
    {

        if (this.draw != this.EMPTY_FUNCTION)
        {
            this._oldDraw = this.draw;

            this.draw = this.EMPTY_FUNCTION;
        }

    };

    /**
        Shows the sprite
        
    */
    this.show = function()
    {
        this.draw = this._oldDraw;
    };

    /**
        Has the sprite animation to be updated?
    */
    this.animated = false;

    /**
        Updates the spirte animation
        
        @param {number} dt Elapsed time with which update the animation in msec.
    */
    this.update = function(dt)
    {
        if (this.animated)
        {
            this.anims[this.curAnim].update(dt);
        }
    }

    /**
        Tests if a sprite is over (aka z-indexing) this sprite
        
        @param {Sprite} sprite the other sprite to test.
    */
    this.isOver = function(sprite)
    {
        //test if this is over another sprite
        var myLayerIdx = GNGraphics.layerOrder.indexOf(this.layer);
        var otherLayerIdx = GNGraphics.layerOrder.indexOf(sprite.layer);

        if(myLayerIdx < otherLayerIdx) return true;
        else
        {
            if(myLayerIdx == otherLayerIdx)
            {
                if(this.layerPosId > sprite.layerPosId) return true;
            }
        }
        return false;
    }

}


/**
    Rectangle Sprite
    @constructor
*/
function Rect(name,w,h){

    this.name = name;

    this.color= {r:255,g:0,b:0};
    this.alpha = 1; //alpha is from 0 a 1
    this.fill = true;
    this.stroke = false;
    this.width = w || 100;
    this.height = h || 100;
    this.x = 0;
    this.y = 0;

    /**
        Draws the rect sprite.
    */
    this.draw = function(){

       

        if(!this.fill)
        {
             this.GNGraphics.context.globalAlpha = this.alpha;
             this.GNGraphics.context.strokeStyle = "rgb("+this.color.r+","+this.color.g+","+this.color.b+")";
             this.GNGraphics.context.strokeRect(this.x,this.y, this.width, this.height);
           
        }
        else
        {
             this.GNGraphics.context.globalAlpha = this.color.a;
             this.GNGraphics.context.fillStyle = "rgb("+this.color.r+","+this.color.g+","+this.color.b+")";
             this.GNGraphics.context.fillRect(this.x,this.y, this.width, this.height);
        }

        
        

    }
}


    /**
        Sprite Manager Class
        @constructor
    */
    function GNGraphics()
    {

        /**
            Actual canvas element
        */
        this.canvas = null;

        /**
            Actual canvas context
            @property
        */
        this.context = null;

        // contains all the created sprites
        this.spriteTable = {};

        /**
            drawcalls counter
            @inner
        */
        this.drawCallsCnt =0;

        /**
            Number of drawcalls in the last frame
            @property
        */
        this.drawCalls =0;

        /**
            Table of the loaded images
            @property
        */
        this.images = {};

        /**
            2D Render mode constant
            @property
        */
        this.RENDER_MODE2D = "2D";

        /**
            3D Render mode constant
            @property
        */
        this.RENDER_MODE3D = "3D";
        /**
            Initializes the canvas object for rendering
            @function
            @param {object} canvas The canvas element to initialize
            @param {string} renderMode Specify the rendering mode to use 2D or 3D
        */
        this.initCanvas = function(canvas,renderMode){

            canvas = canvas || document.getElementsByTagName('canvas')[0];
            renderMode = renderMode || this.RENDER_MODE2D;

            // 2d render mode init
            if(renderMode == this.RENDER_MODE2D){

                this.canvas = canvas;
                this.context = canvas.getContext("2d");
            }
            // 3d render mode init
            else
            {
                console.warn("Sorry, no 3D context support available yet.")
            }

        }

        /**
            Loads a single image
            @function
            @param {string} url Url of the image to load
            @param {object} callback Function to call when the image is loaded
        */
        this.loadImage = function(url,callback){

            var img = new Image();
            img.addEventListener('load',callback);

            // get the name
            var name = url.substring(url.lastIndexOf("/")+1);
            img.name = name.substring(0,name.indexOf("."));
            img.src = url;

        };

        /**

             Loads An image
             @function
             @param {object} imagesUrls An array of string specifying the urls of the images to be loaded
             @param {object} callback  Callback function object to call at the end of loading
        */
        this.loadImages = function(imagesUrls, callback)
        {
            if(imagesUrls){

                // creates a loading barrier
                this.loadBarrierSize = imagesUrls.length;
                this.loadUserCallback = callback || function(){};
                /**
                    @private
                */
                var loadBarrierCallback = function(e){


                    var img = e.target;

                    GNGraphics.images[img.name] = img;

                    GNGraphics.loadBarrierSize --;

                    if(GNGraphics.loadBarrierSize <=0)
                    {
                        GNGraphics.loadUserCallback();

                        //delete GNGraphics.loadBarrierSize;
                        //delete GNGraphics.loadUserCallback;
                    }
                    
                       
                }
                          
               for(var i=0; i<imagesUrls.length; i++){

                    this.loadImage(imagesUrls[i],loadBarrierCallback);
               }
            }
        };

        /**
            Creates a new Sprite
        */
        this.createSprite = function(imageTable,spriteName,w,h,layerName){



            var sprite = new Sprite(imageTable, spriteName,w || imageTable[0][0].width,h || imageTable[0][0].height);
            sprite.GNGraphics = this;

            layerName = (layerName || "main");

            //this.layers[layerName] = this.layers[layerName] == null ? [] : this.layers[layerName];
            if(!this.layers[layerName]) this.addLayer(layerName);

            this.layers[layerName].push(sprite);

            this.spriteTable[spriteName] = sprite;

            sprite.layer = layerName;
            sprite.layerPosId =  this.layers[layerName].length-1;


            return sprite;

        };

        /**
            Creates a Rect Sprite
        */
        this.createRect = function(name,w,h)
        {
            var r = new Rect(name,w,h);

            r.GNGraphics = this;
            this.spriteTable[name] = r;

            return r;

        };


        /**
            Removes the sprite 
        */
        this.removeSprite = function(spriteName){

            var sprite = this.spriteTable[spriteName];

            if(sprite == null)return;

            // removes the sprite from its layer
            var layer = this.layers[sprite.layer];

            layer.splice(sprite.layerPosId,1);

            // recalc pos id
            var z = 0;
            for(var i in layer)
            {
                var sprite = layer[i];

                sprite.layerPosId = z;
                z++;
            }

            delete this.spriteTable[spriteName];
        }

        // list of all the layers in the graphics system
        this.layers = { "main":[] };

        // oreder of draw of layers (aka z-indexing)
        // id=0 is the layer on the top
        this.layerOrder = ["main"];

        /**
            add a new layer
            @param {layerName} name for the new layer
        */
        this.addLayer = function (layerName)
        {
            if(!layerName) return;
            if(this.layers[layerName]) return;

            this.layers[layerName] = [];

            this.layerOrder.push(layerName);
        }

        /**
            remove a layer
            @param {layerName} name for the layer to remove
            @param {removeSprites} if true removes also the sprites contained in the layer, else move it to main layer
        */
        this.removeLayer = function(layerName, removeSprites)
        {
            if(!layerName && layerName == "main") return;
            if(this.layers[layerName])
            {
                var layer = this.layers[layerName];
                var lIdx = this.layerOrder.indexOf(layerName);
                if(lIdx >= 0 && lIdx < this.layerOrder.length) this.layerOrder.splice(lIdx, 1);
                delete this.layers[layerName];

                for(var i in layer)
                {
                    var sprite = layer[i];
                    if(removeSprites)
                    {
                        this.removeSprite(sprite.name);
                    }
                    else
                    {
                        //move sprite in main layer
                        this.changeSpriteLayer(sprite,"main",null);
                    }
                }
            }          
        }

        /**
            change layer position in layer draw order
            @param {layerName} name for the layer
            @param {drawOrderPosition} drawing position in order, layer already in same or bottom position are moved down 
        */
        this.moveLayerAt = function(layerName, drawOrderPosition)
        {
            if(!layerName && layerName == "main") return;
            if(this.layers[layerName])    
            {
                var lIdx = this.layerOrder.indexOf(layerName);
                if(lIdx >= 0 && lIdx < this.layerOrder.length)
                {
                    if(drawOrderPosition >= 0 && drawOrderPosition < this.layerOrder.length)
                    {
                        this.layerOrder.splice(lIdx, 1);
                        this.layerOrder.splice(drawOrderPosition, 0, layerName);
                    }
                }

            }      
        }

        /**
            change layer position and move it on top
            @param {layerName} name for the layer
        */
        this.moveLayerToFront = function(layerName)
        {
            this.moveLayerAt(layerName, 0);
        }

        /**
            change layer position and move it on bottom
            @param {layerName} name for the layer
        */
        this.moveLayerToBack = function(layerName)
        {
            this.moveLayerAt(layerName, this.layerOrder.length-1);
        }

        /**
            clears the screen
        @function

        */
        this.clear = function(){

            this.context.clearRect(0,0,this.canvas.width,this.canvas.height);
        };

       
        /**
            Draws all sprites
        */
        this.draw = function(){

            this.clear();

            var layer = null; 
            
            var sprite = null;

            var ll = this.layerOrder.length-1;

            for(var lk = ll; lk >= 0; lk--){

                layer = this.layers[this.layerOrder[lk]];

                for(var i=0; i<layer.length; ++i){

                    sprite = layer[i];

                    sprite.draw();

                }
            }

            this.drawCalls = this.drawCallsCnt;

            this.drawCallsCnt = 0;
        }

         /**
             @function
             Update all sprite animations
        */
        this.update = function(dt){

            var layer = null; 
            
            var sprite = null;

            var ll = this.layerOrder.length-1;

            for(var lk = ll; lk >= 0; lk--){

                layer = this.layers[this.layerOrder[lk]];

                for(var i=0; i<layer.length; ++i){

                    sprite = layer[i];

                    sprite.update(dt);
                }
            }

        }

        /**
        *
        *   Gets the specified batch
        *   
        *   @param {String} name    A string with the name of the batch to get.
        *   @return {Object} The batch specified by name or null.
        */
        this.layer = function(name)
        {
            return this.layers[name];
        };

        /**
            Gets the specified sprite
        */
        this.sprite = function(name){

            return this.spriteTable[name];
        }

        /** 

            Moves a sprite within his layer

        */
        this.moveSpriteToLayerPos = function(sprite,index){

            var layerName = sprite.layer;

            var layer = this.layers[layerName];

            // remove the sprite from its current pos
            layer.splice(sprite.layerPosId,1);

            // add it ot the new pos
            layer.splice(index,0,sprite);

            // updates the sprite
            sprite.layerPosId = index;

        }

        /**
            Move the sprite on top of the others on the layer
        */
        this.moveSpriteToFront = function(sprite){

            var lastPos = this.layers[sprite.layer].length-1;

            this.moveSpriteToLayerPos(sprite,lastPos);

        }

        /**
            Move the sprite on bottom of the others on the layer
        */
        this.moveSpriteToBack = function(sprite){


            this.moveSpriteToLayerPos(sprite,0);

        }

        /**
            Changes the drawing layer of the sprite
        */
        this.changeSpriteLayer = function(sprite,newLayerName,index){

            var layerName = sprite.layer;
            var layer = this.layers[layerName];

            // remove the sprite from its current pos
            layer.splice(sprite.layerPosId,1);

            //refresh double references
            for(var i in layer)
            {
                layer[i].layerPosId = i;
            }

            var toLayer = this.layers[newLayerName];
            if(!toLayer)
            {
                this.addLayer(newLayerName);
                toLayer = this.layers[newLayerName];
            }

            if(typeof index != "number") //if index not defined set it as next (last) element
            {
                index = toLayer.length;
            }
            
            toLayer.splice(index, 0, sprite);

            sprite.layer = newLayerName;
            sprite.layerPosId = index;
        };

        
        /**
            @function
            Draws a string of text on screen in immediate mode
            @param  {String} string The string to draw
            @param  {number} x X position in pixels of the text
            @param  {number} y Y position in pixels of the text
            @param  {String} color The color of the text as string
            @param  {String} style The style string in this format [font style][font weight][font size][font face]
            @param  {String} alignment Alignment of the text  ['left','center','right']
        */
        this.drawText = function(string,x,y,color,style,alignment,fill){

                this.context.font = style || "normal 36px Verdana";
                this.context.textAlign = alignment  || "left";

                fill = fill == null? true : fill;


                if(fill){

                    this.context.fillStyle = color || "#000000";

                    this.context.fillText(string || "UNDEFINED",x || 0, y || 0);

                }
                else
                {
                    this.context.strokeStyle = color || "#000000";
                    this.context.strokeText(string || "UNDEFINED",x || 0, y || 0);
                }


        };

        /**
            @function 
            Set the size of the canvas
            @param {number} width Width of the canvas in pixels
            @param {number} height Height of the canvas in pixels
        */
        this.setCanvasContextSize = function(width,height){

            width = width || 1;
            height = height || 1;

            this.canvas.width = width;
            this.canvas.height = height;

        };

        /**
            @function 
            Set the size of the canvas's frame( style of the DOM Element)
            @param {number} width Width of the canvas in pixels
            @param {number} height Height of the canvas in pixels
        */
        this.setCanvasSize = function(width,height){

            width = width || 1;
            height = height || 1;

            this.canvas.style.width = width + "px";
            this.canvas.style.height = height + "px";

            // resize the canvas
            this.setCanvasContextSize(width,height);

        };


      

    };

    window.GNGraphics = new GNGraphics();

